/*
 * Processor
 *
 * SJSU CS 149 Spring 2009 Project
 *
 * Copyright 2009 Timmy Yee
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * The Processor class simulates a central processing unit (CPU). Its main
 * job is to run the current process. It also takes care of sending and
 * handling interrupts. It stores information such as the time slice
 * (otherwise known as the time quantum) and context switching overhead
 * time.
 *
 * @author Keven Wong
 * @author Timmy Yee
 */
public final class Processor
{
    private static Process running_ = null;
    private static boolean pending_ = false;

    /*
     * The following are pairs of fields that are structured as follows:
     *
     * xTimer_ is some sort of timer that is decremented regularly.
     * xInterval_ is a time setting that is only changeable by the user.
     *
     * Note that xTimer_ is intially set to the value of xInterval_ when
     * the appropriate method is invoked.
     */
    private static int rrInterval_ = 90;
    private static double rrTimer_ = 0;
    private static int switchInterval_ = 0;
    private static double switchTimer_ = 0;
    private static int faultInterval_ = 3;
    private static double faultTimer_ = 0;

    /**
     * A private constructor to prevent instantiating this static class.
     */
    private Processor() {}

    /**
     * Resets the processor back to a clean state.
     */
    public static void reset()
    {
        rrTimer_ = 0;
        switchTimer_ = 0;
        faultTimer_ = 0;
        running_ = null;
        pending_ = false;
    }

    /**
     * Checks for the timer interrupt that is used for round robin.
     */
    private static void checkTimer()
    {
        if (rrInterval_ == 0 || !isRunning() ||
            Scheduler.getProcessAlgorithm() != Algorithm.ROUND_ROBIN)
            return;

        rrTimer_ -= Simulator.TIME_UNIT;
        if (Util.areDoublesEqual(rrTimer_, 0)) {
            Trace.log("Timer interrupt occurred");
            Scheduler.swapProcesses();
        }
    }

    /**
     * Checks if the page fault handling is done. If it isn't, decrement
     * the fault timer to simulate page loading.
     */
    private static void checkPageFault()
    {
        if (!isHandlingPageFault())
            return;

        faultTimer_ -= Simulator.TIME_UNIT;
        if (Util.areDoublesEqual(faultTimer_, 0))
            Trace.log("Page swap completed (pid: %d, page#: %d)",
                        running_.getId(), running_.getRequestedPage());
    }

    /**
     * Checks if the trace has to display the various simulation data
     * information according to the time interval set by the user.
     */
    private static void checkInfoInterval()
    {
        double currentTime = Simulator.getCurrentTime();
        double intervalTime = Display.getDisplayInterval();

        if (intervalTime == 0)
            return;

        if (Util.nearestTenth(currentTime) % intervalTime == 0)
            Trace.log(Scheduler.getInfo());
    }

    /**
     * Checks if a device interrupt has occurred.
     *
     * @param d The device to check an interrupt from.
     */
    private static void checkDevice(Device d)
    {
        Process p = Scheduler.getProcessFromDeviceQ(d);
        String device;

        if (p == null)
            return;

        Scheduler.offerToReadyQ(p);

        if (d == Device.DISK)
            device = "Disk";
        else if (d == Device.PRINTER)
            device = "Printer";
        else
            throw new IllegalArgumentException("invalid device");

        Trace.log("%s interrupt occurred", device);
        Trace.log("%s operation terminated for process %d", device, p.getId());
        Trace.log("Process %d moved from %s Queue to Ready Queue",
                  p.getId(), device);
    }

    /**
     * Handles each type of interrupt.
     */
    public static void handleInterrupts()
    {
        // let's not do interrupts right when the simulator starts up
        if (Simulator.getCurrentTime() == 0)
            return;

        // poll each possible type of interrupt
        checkTimer();
        checkPageFault();
        checkInfoInterval();
        checkDevice(Device.DISK);
        checkDevice(Device.PRINTER);
    }

    /**
     * Gets the time slice for all processes.
     *
     * @return The max time period that any process is given.
     */
    public static int getTimeSlice()
    {
        return rrInterval_;
    }

    /**
     * Sets the time slice for all processes.
     *
     * @param time The max time period that any process is given.
     */
    public static void setTimeSlice(int time)
    {
        rrInterval_ = time;
    }

    /**
     * Gets the amount of time for context switching.
     *
     * @return The processor overhead time when switching processes.
     */
    public static int getContextSwitch()
    {
        return switchInterval_;
    }

    /**
     * Sets the amount of time for context switching.
     *
     * @param contextSwitch The processor overhead time
     *                      when switching processes.
     */
    public static void setContextSwitch(int contextSwitch)
    {
        switchInterval_ = contextSwitch;
    }

    /**
     * Gets the amount of time for page fault handling.
     *
     * @return The page fault handling time when loading a page.
     */
    public static int getFaultInterval()
    {
        return faultInterval_;
    }

    /**
     * Gets the process running on this processor.
     *
     * @return The current running process.
     */
    public static Process getRunningProcess()
    {
        return running_;
    }

    /**
     * Gets the priority rank of the process running on this processor.
     *
     * @return The priority of the running process.
     */
    public static int getRunningPriority()
    {
        return running_.getPriority();
    }

    /**
     * Attempts to assign a process from the ready queue to the processor.
     */
    public static void setRunningProcess()
    {
        running_ = Scheduler.pollFromReadyQ();

        if (!isRunning())
            return;

        rrTimer_ = 0;
        switchTimer_ = switchInterval_;

        if (switchTimer_ == 0)
            finallySwitch();
        else
            running_.incrementSwitchCounter();
    }

    /**
     * Terminates the running process.
     */
    public static void killRunningProcess()
    {
        Trace.log("Process %d terminated", running_.getId());
        running_.setTurnaroundTime();
        Scheduler.buryProcess();
        running_ = null;
    }

    /**
     * Checks if processor is currently in a context switch.
     *
     * @return True if switching to a process, false otherwise.
     */
    public static boolean hasOverhead()
    {
        return !Util.areDoublesEqual(switchTimer_, 0);
    }

    /**
     * Continues the context switch.
     */
    public static void continueSwitch()
    {
        switchTimer_ -= Simulator.TIME_UNIT;

        if (Util.areDoublesEqual(switchTimer_, 0))
            pending_ = true;
    }

    /**
     * Switches to the process finally when the context switch is over.
     */
    private static void finallySwitch()
    {
        if (running_.hasNotBeenAssignedBefore())
            running_.setResponseTime();

        rrTimer_ = rrInterval_;
        Trace.log("Process %d assigned to the CPU", running_.getId());
    }

    /**
     * Checks to see if the processor is busy handling a page fault.
     *
     * @return True if busy handling, false otherwise.
     */
    private static boolean isHandlingPageFault()
    {
        return !Util.areDoublesEqual(faultTimer_, 0);
    }

    /**
     * Determines whether there is an active process running.
     * <p>
     * Note that an active process means any process that is not the
     * ancestor of all processes: idle. The idle process is simulated as
     * the null reference assigned to <code>running_</code>.
     *
     * @return True if there is a process is running, false otherwise.
     */
    public static boolean isRunning()
    {
        return running_ != null;
    }

    /**
     * Runs the process without fear of it being null.
     */
    private static void runWild()
    {
        if (pending_) {
            finallySwitch();
            pending_ = false;
        }

        if (hasOverhead()) {
            continueSwitch();
            return;
        }

        if (running_.isDone()) {
            killRunningProcess();
            setRunningProcess();
        }

        if (running_.hasPageFaultOccurred()) {
            running_.loadPage();
            faultTimer_ = faultInterval_;
            return;
        }

        // check for system calls only every msec
        if (Util.isWholeNumber(Simulator.getCurrentTime())) {
            boolean success = Scheduler.assignToDevice(running_);

            /*
             * If the running process is successfully assigned to a device,
             * it will be blocked and some other process will take its
             * place. The new running process should wait until the next
             * clock cycle to do work on the processor.
             */
            if (success)
                return;
        }

        running_.accessPage();
        running_.doWork();
    }

    /**
     * Runs the process.
     */
    public static void run()
    {
        if (isHandlingPageFault())
            return;

        try {
            runWild();
        } catch (NullPointerException e) {
            return;
        }
    }
}

